import numpy as np
from view.endSrc.tDataset import tDataset
from view.endSrc.MyTools import MyTools

class Dataset(np.ndarray):
    '''
        This class server two goals:
        1. represent the synthetic dataset in memory, not stored into DB yet.
           such as MixedGaussianDataset, MixedGammaDataset,

           and store the dataset into DB.

        2. load old dataset from DB table tDataset
    '''
    
    def __new__(cls, datasetMat, labels, trueCentres=None, className='Dataset'):
        '''
            The factory used to create objects of Dataset from memory
        :param datasetMat:
        :param labels:
        :param trueCentres:
        :param className:
        :return:
        '''

        # only check datasetMat simply, further checking will be done in __init__
        assert isinstance(datasetMat, np.ndarray) and \
            len(datasetMat.shape) == 2, 'datasetMat must be np.ndarray with shape like (n, d)'
        self = datasetMat.view(cls)
        return self


    def __init__(self, datasetMat, labels, trueCentres=None, className='Dataset'):
        '''

        :param datasetMat:
        :param labels:
        :param trueCentres:  if it is None, means a user do not know the true centers, such as real-word datasets
        :param className:
        '''

        ######################################
        # check dimensions, shape
        nRows = np.shape(datasetMat)[0]
        assert nRows == len(labels), 'length of labels must be equal to numOfRows of datasetMat'
        labels = np.asarray(labels).astype(int)

        vecShp = np.shape(datasetMat[0])
        if trueCentres is not None:
            MyTools.checkMemberShape(trueCentres, vecShp, 'Dataset.__init__.trueCentres')


        #########################################################
        # python variables for DB fields of tDataset
        # self.id = None
        # self.des = 'a dataset'              # NN: not necessary, set by non-initial method
        # self.fileName = None                # NN
        # self.nRows = None                   # NN
        # self.nCols = None                   # NN
        # self.className = None        # must set by init  **
        # self.nClusters = None               # NN
        # self.trueCentresFileName = None     # NN
        # self.mixedParams = None             # set by non-initial method
        # self.name = None                    # set by non-initial method
        #
        # self.m_dataset = None       # must set by init **
        # self.m_labels = None        # must set by init **
        # self.m_trueCentres = None   # must set by init **

        self.t = tDataset(None)  # set sqlConn = None, later set it.

        self.t.className = className

        # self.t.m_trueCentres = trueCentres
        # self.t.m_labels = labels  DONT DO this , use the following instead
        # self.t.m_dataset = datasetMat  DONT DO this , use the following instead
        self.t.setBaseInfo(datasetMat, labels, trueCentres)

        self.clusterIdsList = None  # computed field from labels

    def __str__(self):
        '''
            for simple test
        :return:
        '''

        tmp = '---------- Dataset----------------\n'
        tmp += str(self.t)

        tmp += 'clusterIdsList: \n'
        self.getIdsOfEachCluster()
        for ids in self.clusterIdsList:
            tmp += str(ids) + '\n'
        return tmp

    @staticmethod
    def fromDB(sqlConn, dbId):
        '''
            The factory is used to create objects of Dataset from DB
        :param sqlConn:
        :param dbId:
        :return:
        '''

        t = tDataset(sqlConn)
        if not t.readDataset(dbId):
            sqlConn.logger.write('Dataset.fromDB..failed to read dataset from dbId= ' + str(dbId))
            return None

        ds = Dataset(t.m_dataset, t.m_labels, t.m_trueCentres, t.className)
        ds.t = t    # set it back, since here all members of t are validated, gotten from DB
        return ds

    def setDescription(self, des):
        self.t.des = des

    def setName(self, name):
        self.t.name = name

    def getLabels(self):
        return self.t.m_labels

    def getTrueCentres(self):
        return self.t.m_trueCentres

    def getMixedParams(self):
        '''
            return mixed parameters used to generate this dataset,
            so that user can re-generate a new dataset like this one.

        :return: the mixedParams DB field, whose type is dict and all other fields are list, list of list
                 i.e. JSON dict
        '''
        return self.t.mixedParams

    def saveToDB(self, sqlConn, mixedParams=None):
        '''
        :param datasetName:
        :param mixedParams: JSON dict
        :return: dbId if successful, None otherwise
        '''

        if not MyTools.isJsonDumpable(mixedParams):
            self.sqlConn.logger.write('Dataset.saveToDB..failed for mixedParams is not JSON dumpable: ' + str(mixedParams))
            return None

        self.t.setSqlConn(sqlConn)
        return self.t.saveDataset(self.t.name, self.t.className, self, self.t.m_labels,
                           self.t.des, self.t.m_trueCentres, mixedParams)


    def getNumOfClusters(self):

        return self.t.nClusters


    def _computeClusterIdsList(self):

        if self.clusterIdsList is not None:
            return

        self.clusterIdsList = []
        for i in range(self.t.nClusters):
            self.clusterIdsList.append([])

        # self.clusterIdsList = [[]] * self.t.nClusters, DONT USE THIS, since it will copy reference of internal []
        for i, cId in enumerate(self.t.m_labels):
            self.clusterIdsList[cId].append(i)

    def getIdsOfEachCluster(self):
        
        '''
        This method returns the clusterIds for each cluster of a dataset.

        Return
        --------
        clusterIdsList: a list containing k lists of indexes that belong to the cluster.
        For example, [0, 0, 1, 2, 0, 2, 1] is the self.clusterLabels,
        then return [[0, 1, 4], [2, 6], [3, 5]]

        Examples:
        --------
        dataset.getIdsOfEachCluster()
        '''

        self._computeClusterIdsList()

        return self.clusterIdsList

    def getIds(self, clusterId):

        '''
            Return the clusterIds for one specific cluster of a dataset.

        Return
        --------
        clusterIdsList: a list containing indexes that belong to this cluster.
        For example, [0, 0, 1, 2, 0, 2, 1] is the self.clusterLabels and clusterId = 1
        then return [2, 6]

        Examples:
        --------
        dataset.getIds(1)
        '''

        self._computeClusterIdsList()

        return self.clusterIdsList[clusterId]


    def getNumOfRecords(self):

        return self.t.nRows

    def getNumOfAttributes(self):

        return self.t.nCols

